﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Xml.Serialization;

namespace Zeus.Core.Extension.Basic
{
    /// <summary>
    /// Object Extensions.
    /// </summary>
    public static class ObjectExtensions
    {
        /// <summary>
        /// Invokes the method by the current instance, using the specified parameters.
        /// </summary>
        /// <param name="source">The instance of the invoked method.</param>
        /// <param name="method">The name of the invoked method.</param>
        /// <param name="parameters">The parameters of the invoked method.</param>
        /// <returns>The return value of the invoked method.</returns>
        public static object InvokeMethod(this object source, string method, object[] parameters)
        {
            MethodInfo methodInfo = source.GetType().GetMethod(method);
            return methodInfo.Invoke(source, parameters);
        }

        /// <summary>
        /// If object is not null, invoke method.
        /// </summary>
        /// <param name="source">Object to check.</param>
        /// <param name="action">Delegate method.
        /// <example>E.g. <code>source => DoSomething(source);</code></example>
        /// </param>
        /// <returns>Source object.</returns>
        public static object IfNotNull(this object source, Action<object> action)
        {
            if (source != null)
            {
                action(source);
            }

            return source;
        }

        /// <summary>
        /// If object is null, invoke method.
        /// </summary>
        /// <param name="source">Object to check.</param>
        /// <param name="action">Delegate method.
        /// <example>E.g. <code>source => DoSomething(source);</code></example>
        /// </param>
        public static void IfNull(this object source, Action<object> action)
        {
            if (source == null)
            {
                action(source);
            }
        }

        /// <summary>
        /// Invoke System.Console.WriteLine() or System.Console.Write().
        /// </summary>
        /// <typeparam name="T">The type of input object.</typeparam>
        /// <param name="source">The input object.</param>
        /// <param name="obj">Append object to display.</param>
        /// <param name="withNewLine">Whether followed by the current line terminator.</param>
        /// <returns>The input object.</returns>
        public static T ConsoleOutput<T>(this T source, object obj, bool withNewLine = true)
        {
            if (object.ReferenceEquals(obj, null))
            {
                if (withNewLine)
                {
                    Console.WriteLine(source);
                }
                else
                {
                    Console.Write(source);
                }
            }
            else
            {
                if (withNewLine)
                {
                    Console.WriteLine("{0}{1}", source, obj);
                }
                else
                {
                    Console.Write("{0}{1}", source, obj);
                }
            }

            return source;
        }

        /// <summary>
        /// Invoke System.Console.WriteLine() or System.Console.Write().
        /// </summary>
        /// <typeparam name="T">The type of input object.</typeparam>
        /// <param name="source">The input object.</param>
        /// <param name="format">A composite format string.</param>
        /// <param name="withNewLine">Whether followed by the current line terminator.</param>
        /// <returns>The input object.</returns>
        public static T ConsoleOutput<T>(this T source, string format = null, bool withNewLine = true)
        {
            if (string.IsNullOrEmpty(format))
            {
                if (withNewLine)
                {
                    Console.WriteLine(source);
                }
                else
                {
                    Console.Write(source);
                }
            }
            else
            {
                if (format.Contains("{0}"))
                {
                    if (withNewLine)
                    {
                        Console.WriteLine(format, source);
                    }
                    else
                    {
                        Console.Write(format, source);
                    }
                }
                else
                {
                    if (withNewLine)
                    {
                        Console.WriteLine("{0}{1}", source, format);
                    }
                    else
                    {
                        Console.Write("{0}{1}", source, format);
                    }
                }
            }

            return source;
        }

        /// <summary>
        /// Perform a deep Copy of the object.
        /// </summary>
        /// <typeparam name="T">The type of input object.</typeparam>
        /// <param name="source">The object instance to copy.</param>
        /// <returns>The copied object.</returns>
        public static T CloneDeep<T>(this T source)
        {
            if (!typeof(T).IsSerializable)
            {
                throw new ArgumentException("The type must be serializable.", "source");
            }

            // Don't serialize a null object, simply return the default for that object
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            MemoryStream memoryStream = null;
            T result = default(T);

            try
            {
                memoryStream = new MemoryStream();
                BinaryFormatter binaryFormatter = new BinaryFormatter();
                binaryFormatter.Serialize(memoryStream, source);
                memoryStream.Seek(0, SeekOrigin.Begin);
                result = (T)binaryFormatter.Deserialize(memoryStream);
                memoryStream.Flush();
            }
            catch
            {
                throw;
            }
            finally
            {
                if (memoryStream != null)
                {
                    memoryStream.Close();
                    memoryStream = null;
                }
            }

            return result;
        }

        /// <summary>
        /// Convert object to bytes.
        /// </summary>
        /// <typeparam name="T">The type of input object</typeparam>
        /// <param name="source">Object.</param>
        /// <returns>Byte array.</returns>
        public static byte[] ToByteArray<T>(this T source)
        {
            if (!typeof(T).IsSerializable)
            {
                throw new ArgumentException("The type must be serializable.", "source");
            }

            // Don't serialize a null object, simply return the default for that object
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            MemoryStream memoryStream = null;
            byte[] result = null;

            try
            {
                memoryStream = new MemoryStream();
                BinaryFormatter binaryFormatter = new BinaryFormatter();
                binaryFormatter.Serialize(memoryStream, source);
                result = memoryStream.ToArray();
                memoryStream.Flush();
            }
            catch
            {
                throw;
            }
            finally
            {
                if (memoryStream != null)
                {
                    memoryStream.Close();
                    memoryStream = null;
                }
            }

            return result;
        }

        /// <summary>
        /// Serializes an object to a JSON string.
        /// </summary>
        /// <param name="source">Object to serialize.</param>
        /// <returns>JSON string.</returns>
        public static string ToJson(this object source)
        {
            // Don't serialize a null object, simply return the default for that object
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            MemoryStream memoryStream = null;
            string result = null;

            try
            {
                memoryStream = new MemoryStream();
                var serializer = new DataContractJsonSerializer(source.GetType());
                serializer.WriteObject(memoryStream, source);
                result = Encoding.Default.GetString(memoryStream.ToArray());
                memoryStream.Flush();
            }
            catch
            {
                throw;
            }
            finally
            {
                if (memoryStream != null)
                {
                    memoryStream.Close();
                    memoryStream = null;
                }
            }

            return result;
        }

        /// <summary>
        /// Serializes an object to a JSON string.
        /// </summary>
        /// <param name="source">Object to serialize.</param>
        /// <param name="knownTypes">An IEnumerable of known types.  Useful for complex objects.</param>
        /// <returns>JSON string.</returns>
        public static string ToJson(this object source, IEnumerable<Type> knownTypes)
        {
            // Don't serialize a null object, simply return the default for that object
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            MemoryStream memoryStream = null;
            string result = null;

            try
            {
                memoryStream = new MemoryStream();
                var serializer = new DataContractJsonSerializer(source.GetType(), knownTypes);
                serializer.WriteObject(memoryStream, source);
                result = Encoding.Default.GetString(memoryStream.ToArray());
                memoryStream.Flush();
            }
            catch
            {
                throw;
            }
            finally
            {
                if (memoryStream != null)
                {
                    memoryStream.Close();
                    memoryStream = null;
                }
            }

            return result;
        }

        /// <summary>
        /// Serializes a JSON object to an object.
        /// </summary>
        /// <typeparam name="T">Type of the <paramref name="returns"/> objet.</typeparam>
        /// <param name="source">JSON string object.</param>
        /// <returns>The result object.</returns>
        public static T FromJson<T>(this string source)
        {
            if (string.IsNullOrEmpty(source))
            {
                return default(T);
            }

            MemoryStream memoryStream = null;
            T result = default(T);

            try
            {
                memoryStream = new MemoryStream(Encoding.Default.GetBytes(source));
                var serializer = new DataContractJsonSerializer(typeof(T));
                result = (T)serializer.ReadObject(memoryStream);
                memoryStream.Flush();
            }
            catch
            {
                throw;
            }
            finally
            {
                if (memoryStream != null)
                {
                    memoryStream.Close();
                    memoryStream = null;
                }
            }

            return result;
        }

        /// <summary>
        /// Serializes a JSON object to an object.
        /// </summary>
        /// <typeparam name="T">Type of the <paramref name="returns"/> objet.</typeparam>
        /// <param name="source">JSON string object.</param>
        /// <param name="knownTypes">An IEnumerable of known types. Useful for complex objects.</param>
        /// <returns>The result object.</returns>
        public static T FromJson<T>(this string source, IEnumerable<Type> knownTypes)
        {
            if (string.IsNullOrEmpty(source))
            {
                return default(T);
            }

            MemoryStream memoryStream = null;
            T result = default(T);

            try
            {
                memoryStream = new MemoryStream(Encoding.Default.GetBytes(source));
                var serializer = new DataContractJsonSerializer(typeof(T), knownTypes);
                result = (T)serializer.ReadObject(memoryStream);
                memoryStream.Flush();
            }
            catch
            {
                throw;
            }
            finally
            {
                if (memoryStream != null)
                {
                    memoryStream.Close();
                    memoryStream = null;
                }
            }

            return result;
        }

        /// <summary>
        /// Serializes the object into an XML string using Encoding.Default.
        /// </summary>
        /// <remarks>
        /// The object to be serialized should be decorated with the
        /// <see cref="SerializableAttribute"/>, or implement the <see cref="ISerializable"/> interface.
        /// </remarks>
        /// <param name="source">The object to serialize.</param>
        /// <returns>An XML encoded string representation of the source object.</returns>
        public static string ToXml(this object source)
        {
            return source.ToXml(Encoding.Default);
        }

        /// <summary>
        /// Serializes the object into an XML string.
        /// </summary>
        /// <remarks>
        /// The object to be serialized should be decorated with the
        /// <see cref="SerializableAttribute"/>, or implement the <see cref="ISerializable"/> interface.
        /// </remarks>
        /// <param name="source">The object to serialize</param>
        /// <param name="encoding">The Encoding scheme to use when serializing the data to XML.</param>
        /// <returns>An XML encoded string representation of the source object.</returns>
        public static string ToXml(this object source, Encoding encoding)
        {
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            if (encoding == null)
            {
                throw new ArgumentNullException("encoding");
            }

            MemoryStream memoryStream = null;
            string result = null;

            try
            {
                memoryStream = new MemoryStream();
                XmlSerializer xmlSerializer = new XmlSerializer(source.GetType());
                xmlSerializer.Serialize(memoryStream, source);
                memoryStream.Position = 0;
                result = encoding.GetString(memoryStream.ToArray());
                memoryStream.Flush();
            }
            catch
            {
                throw;
            }
            finally
            {
                if (memoryStream != null)
                {
                    memoryStream.Close();
                    memoryStream = null;
                }
            }

            return result;
        }

        /// <summary>
        /// Deserializes the XML string object into object.
        /// </summary>
        /// <typeparam name="T">Type of the <paramref name="returns"/> object.</typeparam>
        /// <param name="source">The XML string to deserialize.</param>
        /// <returns>Object.</returns>
        public static T FromXml<T>(this string source)
        {
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            TextReader inputStream = null;
            T result = default(T);

            try
            {
                inputStream = new StringReader(source);
                XmlSerializer xmlSerializer = new XmlSerializer(typeof(T));
                result = (T)xmlSerializer.Deserialize(inputStream);
            }
            catch
            {
                throw;
            }
            finally
            {
                if (inputStream != null)
                {
                    inputStream.Close();
                    inputStream = null;
                }
            }

            return result;
        }

        /// <summary>
        /// Converts an object to the specified target type
        /// or returns the default value if those 2 types are not convertible.
        /// <para>Any exceptions are optionally ignored (<paramref name="ignoreException"/>).</para>
        /// <para>
        /// If the exceptions are not ignored and the <paramref name="source"/> can't be convert even if
        /// the types are convertible with each other, an exception is thrown.</para>
        /// </summary>
        /// <typeparam name = "T">The type of <paramref name="returns"/> object.</typeparam>
        /// <param name = "source">The value.</param>
        /// <param name = "defaultValue">The default value.</param>
        /// <param name = "ignoreException">if set to <c>true</c> ignore any exception.</param>
        /// <returns>The target type</returns>
        public static T ConvertTo<T>(this object source, T defaultValue = default(T), bool ignoreException = true)
        {
            if (ignoreException)
            {
                try
                {
                    return source.ConvertTo<T>(defaultValue);
                }
                catch
                {
                    return defaultValue;
                }
            }

            return source.ConvertTo<T>(defaultValue);
        }

        /// <summary>
        /// Copies the readable and writable public property values from the target object to the source.
        /// </summary>
        /// <remarks>The source and target objects must be of the same type.</remarks>
        /// <param name="source">The source object.</param>
        /// <param name="target">The target object.</param>
        public static void CopyPropertiesFrom(this object source, object target)
        {
            source.CopyPropertiesFrom(target, string.Empty);
        }

        /// <summary>
        /// Copies the readable and writable public property values from the target object to the source and
        /// optionally allows for the ignoring of any number of properties.
        /// </summary>
        /// <remarks>The source and target objects must be of the same type.</remarks>
        /// <param name="source">The source object.</param>
        /// <param name="target">The target object.</param>
        /// <param name="ignoreProperty">A single property name to ignore.</param>
        public static void CopyPropertiesFrom(this object source, object target, string ignoreProperty)
        {
            source.CopyPropertiesFrom(target, new string[] { ignoreProperty });
        }

        /// <summary>
        /// Copies the readable and writable public property values from the target object to the source and
        /// optionally allows for the ignoring of any number of properties.
        /// </summary>
        /// <remarks>The source and target objects must be of the same type.</remarks>
        /// <param name="source">The source object.</param>
        /// <param name="target">The target object.</param>
        /// <param name="ignoreProperties">An array of property names to ignore.</param>
        public static void CopyPropertiesFrom(this object source, object target, string[] ignoreProperties)
        {
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            if (target == null)
            {
                throw new ArgumentNullException("target");
            }

            // Get and check the object types
            Type type = target.GetType();
            if (source.GetType() != type)
            {
                throw new ArgumentException("The target type must be the same as the source");
            }

            // Build a clean list of property names to ignore
            List<string> ignoreList = new List<string>();
            foreach (string item in ignoreProperties)
            {
                if (!string.IsNullOrEmpty(item) && !ignoreList.Contains(item))
                {
                    ignoreList.Add(item);
                }
            }

            // Copy the properties
            foreach (PropertyInfo property in type.GetProperties())
            {
                if (property.CanWrite && property.CanRead && !ignoreList.Contains(property.Name))
                {
                    object val = property.GetValue(target, null);
                    property.SetValue(source, val, null);
                }
            }
        }

        /// <summary>
        /// Converts an object to the specified target type or returns the default value if
        /// those 2 types are not convertible.
        /// <para>Any exceptions are optionally ignored (<paramref name="ignoreException"/>).</para>
        /// <para>
        /// If the exceptions are not ignored and the <paramref name="source"/> can't be convert even if
        /// the types are convertible with each other, an exception is thrown.</para>
        /// </summary>
        /// <typeparam name = "T"></typeparam>
        /// <param name = "source">The value.</param>
        /// <param name = "defaultValue">The default value.</param>
        /// <returns>The target type</returns>
        private static T ConvertTo<T>(this object source, T defaultValue)
        {
            if (source != null)
            {
                var targetType = typeof(T);

                if (source.GetType() == targetType)
                {
                    return (T)source;
                }

                var converter = TypeDescriptor.GetConverter(source);
                if (converter != null)
                {
                    if (converter.CanConvertTo(targetType))
                    {
                        return (T)converter.ConvertTo(source, targetType);
                    }
                }

                converter = TypeDescriptor.GetConverter(targetType);
                if (converter != null)
                {
                    if (converter.CanConvertFrom(source.GetType()))
                    {
                        return (T)converter.ConvertFrom(source);
                    }
                }
            }

            return defaultValue;
        }
    }
}
